package org.dna.metronomo.persistence.impl.annotations;

import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.dna.metronomo.persistence.DBInit;
import org.dna.metronomo.persistence.PersistenceException;

/**
 * Singleton factory to create the session wrapped DAOs
 * @author andrea
 */
public final class DAOFactory {

    static class SessionDecoratorInvocationHandler implements InvocationHandler {
        private static final Logger LOG = Logger.getLogger(SessionDecoratorInvocationHandler.class.getName());
        private Set<String> m_methodsDecorated;
        private Field m_sessionField;
        private SqlSessionFactory m_sqlSessionFactory;
        Object m_wrapped;

        SessionDecoratorInvocationHandler(Set<String> methodsDecorated, Field sessionField,
                Object wrapped,
                SqlSessionFactory sqlSessionFactory) {
            m_methodsDecorated = methodsDecorated;
            m_sessionField = sessionField;
            m_sqlSessionFactory = sqlSessionFactory;
            m_wrapped = wrapped;
        }

        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if (!m_methodsDecorated.contains(method.getName())) {
                LOG.fine("Invoking a not 'sessioned' method " + method.getName());
                return method.invoke(m_wrapped, args);
            }

            //invoke wrapping it with a session
            SqlSession session = m_sqlSessionFactory.openSession();
            m_sessionField.setAccessible(true);
            m_sessionField.set(m_wrapped, session);
            Object res;
            try {
                res = method.invoke(m_wrapped, args);
                session.commit();
            } catch (Exception e) {
                throw new PersistenceException(e);
            } finally {
                session.close();
            }
            
            //clean the closed reference
            m_sessionField.set(m_wrapped, null);
            return res;
        }
    }
    private static final Logger LOG = Logger.getLogger(DAOFactory.class.getName());
    private static DAOFactory s_instance;
    static final String CONF_PATH = "org/dna/metronomo/persistence/configuration.xml";
    SqlSessionFactory m_sqlSessionFactory;

    private DAOFactory() {
        Reader reader;
        try {
            reader = Resources.getResourceAsReader(CONF_PATH);
        } catch (IOException e) {
            LOG.throwing(getClass().getName(), "Constructor", e);
            throw new PersistenceException(e);
        }
        m_sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
    }
    
    /**
     * This is the schema configuration utility.
     *
     * Note that this is automatically configured to run on startup as a spring
     * bean "init-method"
     */
    public synchronized void createSchema() throws PersistenceException {
        DBInit.createSchema();
    }

    public synchronized static DAOFactory getInstance() {
        if (s_instance == null) {
            s_instance = new DAOFactory();
        }
        return s_instance;
    }
    
    public Object createDAO(String clsName) throws ClassNotFoundException {
        Class<?> cls = DAOFactory.class.getClassLoader().loadClass(clsName);
        return createDAO(cls);
    }

    /**
     * Factory method, if it can't return null;
     */
    public static <T> T createDAO(Class<T> cls) {
        getInstance();
        Field sessionField;
        if ((sessionField = s_instance.findSessionField(cls)) == null) {
            return s_instance.constructNormally(cls);
        }
        Set<String> methSet = s_instance.findSessionMethods(cls);
        if (methSet.isEmpty()) {
            return s_instance.constructNormally(cls);
        }

        Object wrapped = s_instance.constructNormally(cls);
        InvocationHandler handler = new SessionDecoratorInvocationHandler(methSet,
                sessionField, wrapped, s_instance.m_sqlSessionFactory);

        T proxed = (T) Proxy.newProxyInstance(cls.getClassLoader(),
                /*new Class[]{cls}*/cls.getInterfaces(),
                handler);
        return proxed;
    }

    /**
     * Return a set of methods names that is tagged with SessionMethod. Empty if 
     * nothing could be found.
     */
    private <T> Set<String> findSessionMethods(Class<T> cls) {
        Method[] methods = cls.getDeclaredMethods();

        Set<String> methNames = new HashSet<String>();
        for (Method meth : methods) {
            if (meth.getAnnotation(SessionMethod.class) != null) {
                methNames.add(meth.getName());
            }
        }
        return methNames;
    }

    /**
     * Search for a field tagged with SessionField annotation, else null if not 
     * found.
     */
    private <T> Field findSessionField(Class<T> cls) {
        //search for fields tagged by @SessionField
        for (Field field : cls.getDeclaredFields()) {
            if (field.getAnnotation(SessionField.class) != null) {
                return field;
            }
        }
        return null;
    }

    /**
     *  Invoke the zero args constructor
     */
    private <T> T constructNormally(Class<T> cls) {
        try {
            return cls.newInstance();
        } catch (InstantiationException ex) {
            LOG.log(Level.SEVERE, null, ex);
        } catch (IllegalAccessException ex) {
            LOG.log(Level.SEVERE, null, ex);
        }
        return null;
    }
}
